import random
import numpy as np

def sigmoid(z):
	return 1.0 / (1.0 + np.exp(-z))

sigmoid_vec = np.vectorize(sigmoid)

def sigmoid_prime(z):
	return sigmoid(z) * (1 - sigmoid(z))

sigmoid_prime_vec = np.vectorize(sigmoid_prime)

class ann():
	def __init__(self,sizes):
		self.num_layers = len(sizes)
		self.sizes = sizes
		self.biases = [np.random.randn(y,1) for y in sizes[1:]]
		self.weights = [np.random.randn(y,x) for x,y in zip(sizes[:-1], sizes[1:])]
	def feedforward(self,a):
		for b, w in zip(self.biases, self.weights):
			a = sigmoid_vec(np.dot(w,a) + b)
		return a
	def SGD(self,training_data,epochs,mini_batch_size,eta,lmbda,test_data = None):
		if test_data:
			ntest = len(test_data)
		n = len(training_data)
		for j in xrange(epochs):
			random.shuffle(training_data)
			mini_batches = [training_data[k:k+mini_batch_size] for k in xrange(0, n, mini_batch_size)]
			for mini_batch in mini_batches:
				self.update_mini_batch(mini_batch,eta,lmbda,n)
			if test_data:
				print "Epoch {0} {1} / {2}".format(j,self.evaluate(test_data), ntest)
			else:
				"Epoch {0} complete".format(j)
	def update_mini_batch(self,mini_batch,eta,lmbda,n):
		nabla_b = [np.zeros(b.shape) for b in self.biases]
		nabla_w = [np.zeros(w.shape) for w in self.weights]

		for x,y in mini_batch:
			delta_nabla_b, delta_nabla_w = self.backprop(x,y)
			nabla_b = [nb+dnb for nb,dnb in zip(nabla_b, delta_nabla_b)]
			nabla_w = [nw+dnw for nw,dnw in zip(nabla_w, delta_nabla_w)]

		self.weights = [(1-(eta*lmbda)/n)*w - (eta/len(mini_batch))*nw for w,nw in zip(self.weights, nabla_w)]
		self.biases = [b - (eta/len(mini_batch))*nb for b,nb in zip(self.biases, nabla_b)]

	def backprop(self,x,y):
		nabla_b = [np.zeros(b.shape) for b in self.biases]
		nabla_w = [np.zeros(w.shape) for w in self.weights]
		#feedforward
		activation = x
		activations = [x]
		zs = []
		for b,w in zip(self.biases, self.weights):
			activation = np.array(activation)
			z = np.dot(w,activation) + b
			zs.append(z)
			activation = sigmoid_vec(z)
			activations.append(activation)
		#backpropagate
		#delta = self.cost_derivative(activations[-1],y) * sigmoid_prime_vec(zs[-1])
		delta = self.cost_derivative(activations[-1],y)
		nabla_b[-1] = delta
		nabla_w[-1] = np.dot(delta, activations[-2].transpose())
		for l in xrange(2,self.num_layers):
			z = zs[-l]
			spv = sigmoid_prime_vec(z)
			delta = np.dot(self.weights[-l+1].transpose(), delta) * spv
			nabla_b[-l] = delta
			nabla_w[-l] = np.dot(delta,activations[-l-1].transpose())
		return nabla_b,nabla_w
	def evaluate(self, test_data):
		test_results = [(np.argmax(self.feedforward(x)),np.argmax(y)) for x,y in test_data]
		return sum(int(x == y) for x,y in test_results)

	def cost_derivative(self, output_activations, y):
		return (output_activations - y)

	def predict(self, data):
		results = [np.argmax(self.feedforward(x)) for x in data]
		return results


